该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,如果能给各位看官带来一丝启发或者帮助,那真是极好的。
前言
在上一篇文章中我们上了一个小例子来自定义View,文章比较简单,阅读量几乎没有,有灌水的嫌疑,(实际上没有,每一篇文章我都是用心在写)。这一篇文章呢,我们来看一下Android事件的分发机制。关于这方面的知识大概已经被讲烂了。我本人也看了好多关于这方面优质的文章和博客。可以说是受益匪浅,但是可是总觉得没有掌握完全。所以我去看了关于底层源码的一些知识。然后在这里分享给大家。
当我们的手指从触摸到屏幕上的各种View开始到这个点击事件结束到底经历了什么,我们来详细分析一下。(Android的输入系统处理了很多事件,包括按键,触摸,以及外接设备,但是我们这篇文章只分析我们最熟悉也是最常用的触摸事件,这里的描述也许不太精确,但是却最为直观)
我们先上一个总体流程图
注:上图中绿色线条表示默认的事件处理流程,即我们没有做任何处理,事件会按照绿色线条所示的方向由Activity->...ViewGroup..->View->...ViewGroup..->Activity这个U型图进行传递。即一直默认调用super.XXX方法。
上图中黑色线条表示默认Activity->...ViewGroup..->View->...ViewGroup..->Activity这个U型图的任一节点中(不包括onInterceptTouchEvent)返回了true,事件即结束,不再向下一节点传递。
上图中红色线条表示一些特殊情况,尤其是ViewGroup,ViewGroup.onInterceptTouchEvent表示询问当前ViewGroup是否需要拦截此事件即要不要处理,为什么要“多此一举”呢,因为ViewGroup.dispatchTouchEvent这个函数的特殊,从上图可知,该函数返回true,是消费事件,返回false是交由上一级的ViewGroup或者Activity的onTouchEvent。那么它怎么向下传递事件或者想把事件交给自己的onTouchEvent处理呢,所以ViewGroup多了个onInterceptTouchEvent(View是没有该函数的),onInterceptTouchEvent起到作用的是分流。onInterceptTouchEvent返回false或者返回super.xxx是向下级View或者ViewGroup传递,返回true呢是把事件交给自己的onTouchEvent处理。
我们知道了上图,,但是Activty的事件又是从哪得到的呢,事件最终返回到Activity的onTouchEvent中又做了什么呢。。下面我们来。。。。。
- 首先从手指触摸到屏幕开始
我们知道Android是基于Linux系统的。当输入设备可用时(这里的输入设备包括很多设备,比如触摸屏和键盘是Android最普遍也是最标准的输入设备,另外它还包括外接的游戏手柄、鼠标等),Linux内核会为输入设置创建对应的设备节点。当输入设备不可用时,就把对应的设备节点删除,这也是如果我们的屏幕意外摔碎了或者其他原因导致触摸屏幕不可用时触摸没有反应的根本原因。当我们的输入设备可用时(我们这里只来讲解触摸屏),我们对触摸屏进行操作时,Linux就会收到相应的硬件中断,然后将中断加工成原始的输入事件并写入相应的设备节点中。而我们的Android 输入系统所做的事情概括起来说就是**监控这些设备节点,当某个设备节点有数据可读时,将数据读出并进行一系列的翻译加工,然后在所有的窗口中找到合适的事件接收者,并派发给它。
- 手指进行一系列操作(这里指的是手指的移动,这一步可能没有)
- 手指抬起或者因其他其他原因(突然间来了个电话之类的)导致事件结束
注:上述第2第3步与第1步里的处理基本相同,但是需要注意的是Android是串行处理事件的,也就是说按下的动作(ACTION_DOWN|ACTION_POINTER_DOWN)处理完成之前是不会处理后续的ACTION_MOVE|ACTION_POINTER_MOVE和ACTION_UP|ACTION_POINTER_UP事件的。并且后续的ACTION_MOVE|ACTION_POINTER_MOVE和ACTION_UP|ACTION_POINTER_UP事件会根据对ACTION_DOWN|ACTION_POINTER_DOWN事件的不同而稍有不同。下面我们先来分析按下的事件ACTION_DOWN|ACTION_POINTER_DOWN的分发。
下面我们来详细分析,请注意,前方高能,请自备纸巾(草稿纸)
INotify和Epoll机制
上面我们说到了Android 输入系统所做的事情概括起来说就是监控设备节点,当某个设备节点有数据可读时,将数据读出并进行一系列的翻译加工,然后在所有的窗口中找到合适的事件接收者,并派发给它。那么它是如何做的呢,,我们来具体分析一下。Android 的输入系统InputManagerService(以下简称为IMS)作为系统服务,它像其他系统服务一样在SystemServer进程中创建。
Linux会为所有可用的输入设备在/dev/input目录在建立event0~n或者其他名称的设备节点,Android输入系统会监控这些设备节点,具体是通过INotify和Epoll机制来进行监控。而不是通过一个线程进行轮询查询。
我们先来看一下INotify和Epoll机制(这里我们只进行简单的描述,读者如果有兴趣可以留言,我单开一篇文章)
INotify机制
INotify是Linux内核提供的一种文件系统变化通知机制。它可以为应用程序监控文件系统的变化,如文件的新建,删除等。
//创建INotify对象,并用描述符inotifyFd 描述它
int inotifyFd = inotify_init();
/*
添加监听
inotify_add_watch函数参数说明
inotifyFd:上面建立的INotify对象的描述符,当监听的目录或文件发生变化时记录在INotify对象
“/dev/input”:被监听的文件或者目录
IN_CREATE | IN_DELETE:事件类型
综合起来下面的代码表示的意思就是当“/dev/input”下发生IN_CREATE | IN_DELETE(创建或者删除)时即把这个事件写入到INotify对象中
*/
int wd = inotify_add_watch(inotifyFd, "/dev/input", IN_CREATE|IN_DELETE )
Epoll机制
在上述INotify机制中我们知道了我们只需关心inotifyFd这个描述符就行了,可是事件是随机发生的,我们也不会本末倒置的采用轮询的方式轮询这个描述符,因为如果这样做的话会浪费大量系统资源。这时候我们Linux的另一个机制就派上用场了,即Epoll机制。Epoll机制简单的说就是使用一次等待来获取多个描述的可读或者可写状态。这样我们不必对每一个描述符创建独立的线程进行阻塞读取,在避免了资源浪费的同时获得较快的相应速度。
至此原始输入事件已经读取完毕,Android输入系统对原始输入事件进行翻译加工以及派发的详细过程很复杂。我们这里只分析其中一部分——IMS与窗口。
Avtivity,Window,PhoneWindow,以及ViewRootImpl之间的联系
上文中我们也说到了IMS会在所有的窗口中找到合适的事件接收者。IMS是运行在SystemServer进程中,而我们的窗口呢,是在我们的应用进程中。这就引出了我们在上文中留下的悬念
// ② 初始化mInputChanel。InputChannel是窗口接收来自InputDispatcher的输入事件的管道。这部分内容我们将在下一篇介绍。
if ((mWindowAttributes.inputFeatures
& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
mInputChannel = new InputChannel();
}
...
...
// ③ 如果mInputChannel不为空,则创建mInputEventReceiver用于接收输入事件。
if (mInputChannel != null) {
mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
Looper.myLooper());
}
读者看到这里该疑惑了,这个不是在ViewRootImpl.setView方法中说的吗,跟现在讲的有关系吗?且听我娓娓道来。在上几篇博客中我们介绍了Avtivity,Window,PhoneWindow,以及ViewRootImpl这些概念之间 到底有什么关系呢。
我们从前几篇中就知道了Activity的启动流程,Activity对象最先创建,但是Activity的显示是依靠其内部对象Window mWindow,而Window是个抽象类,所以mWindow指向的实际上是Window的实现类PhoneWindow的对象。PhoneWindow作为显示的载体,ViewRootImpl的measure、layout以及draw才是View显示的动力所在。我们运行项目,看到了一个MainActivity,我们点击MainActivity的某个View(如Button了或者其他),实际上我们是点击了屏幕上的某个点。由IMS对这个原始事件进行翻译加工并找到我们的PhoneWindow,并向PhoneWindow派发事件。整个过程可用如下流程图表示。
注:上面的流程图中省略了很多细节,意在让读者对Android输入系统有个更整体的把控。
通过上面的流程图我们知道,当我们的PhoneWindow创建完成之后,我们也在该Window上注册了InputChannel并与IMS通信,IMS把事件写入InputChannel,WindowInputEventReceiver对事件进行处理并最终还是通过InputChannel反馈给IMS。
下面我们来稍微介绍下InputChannel和WindowInputEventReceiver。
InputChannel
InputChannel的本质是一对SocketPair(非网络套接字)。套接字可以用于网络通信,也可以用于本机内的进程通信。进程间通信的一种方式,具体解释读者可自行参看《深入理解Android 卷Ⅲ》》中的5.4.1节。
WindowInputEventReceiver
得到InputChannel后,便用它创建WindowInputEventReceiver,WindowInputEventReceiver继承于InputEventReceiver,InputEventReceiver对象可以接收来自InputChannel的输入事件,并触发其onInputEvent方法的回调。我们这里的是WindowInputEventReceiver,所以我们来看一下这个类
final class WindowInputEventReceiver extends InputEventReceiver {
public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) {
super(inputChannel, looper);
}
//重写了onInputEvent方法,所以当InputChannel有事件时,会触发
//WindowInputEventReceiver.onInputEvent(),而其内部直接调用enqueueInputEvent
@Override
public void onInputEvent(InputEvent event) {
enqueueInputEvent(event, this, 0, true);
}
@Override
public void onBatchedInputEventPending() {
if (mUnbufferedInputDispatch) {
super.onBatchedInputEventPending();
} else {
scheduleConsumeBatchedInput();
}
}
@Override
public void dispose() {
unscheduleConsumeBatchedInput();
super.dispose();
}
}
那我们来看一下enqueueInputEvent
void enqueueInputEvent(InputEvent event,
InputEventReceiver receiver, int flags, boolean processImmediately) {
...
//① 将InputEvent对应的InputEventReceiver封装为一个QueuedInputEvent
QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);
//② 将新建的QueuedInputEvent 追加到mPendingInputEventTail所表示的一个单向链表中
QueuedInputEvent last = mPendingInputEventTail;
if (last == null) {
mPendingInputEventHead = q;
mPendingInputEventTail = q;
} else {
last.mNext = q;
mPendingInputEventTail = q;
}
mPendingInputEventCount += 1;
if (processImmediately) {
//③ 如果第三个参数为true,则直接在当前线程中开始对输入事件的处理工作
doProcessInputEvents();
} else {
//④ 否则将处理事件的请求发送给主线程的Handler,随后进行处理
scheduleProcessInputEvents();
}
}
我们来看doProcessInputEvents
void doProcessInputEvents() {
//遍历整个输入事件队列,并逐一处理
while (mPendingInputEventHead != null) {
QueuedInputEvent q = mPendingInputEventHead;
mPendingInputEventHead = q.mNext;
if (mPendingInputEventHead == null) {
mPendingInputEventTail = null;
}
q.mNext = null;
mPendingInputEventCount -= 1;
Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName,
mPendingInputEventCount);
long eventTime = q.mEvent.getEventTimeNano();
long oldestEventTime = eventTime;
if (q.mEvent instanceof MotionEvent) {
MotionEvent me = (MotionEvent)q.mEvent;
if (me.getHistorySize() > 0) {
oldestEventTime = me.getHistoricalEventTimeNano(0);
}
}
mChoreographer.mFrameInfo.updateInputEventTime(eventTime, oldestEventTime);
//deliverInputEvent()方法会将完成单个事件的整个处理流程
deliverInputEvent(q);
}
...
}
而deliverInputEvent方法进行一系列调用最终会调用我们的processPointerEvent()方法
private int processPointerEvent(QueuedInputEvent q) {
final MotionEvent event = (MotionEvent)q.mEvent;
mAttachInfo.mUnbufferedDispatchRequested = false;
mAttachInfo.mHandlingPointerEvent = true;
// 此时ViewRootImpl会将事件的处理权移交给View树的根节点,调用dispatchPointerEvent函数
boolean handled = mView.dispatchPointerEvent(event);
maybeUpdatePointerIcon(event);
maybeUpdateTooltip(event);
mAttachInfo.mHandlingPointerEvent = false;
if (mAttachInfo.mUnbufferedDispatchRequested && !mUnbufferedInputDispatch) {
mUnbufferedInputDispatch = true;
if (mConsumeBatchedInputScheduled) {
scheduleConsumeBatchedInputImmediately();
}
}
return handled ? FINISH_HANDLED : FORWARD;
}
在processPointerEvent我们看到ViewRootImpl会将事件的处理权移交给View树的根节点,调用dispatchPointerEvent函数,即mView,而这个mView就是我们熟知的DecorView
在ActivityThread.handleResumeActivity方法中有如下代码
//decor即DecorView,l是布局参数WindowManager.LayoutParams
wm.addView(decor, l);
触摸事件分发详解
DecorView
我们下面即分析DecorView,我们打开DecorView源码并没有发现dispatchPointerEvent,别着急,别上火,,那么这个dispatchPointerEvent肯定在DecorView父类里面了,,我们打开View源码,,果然找到了,该函数如下
public final boolean dispatchPointerEvent(MotionEvent event) {
if (event.isTouchEvent()) {
//事件如果是Touch事件,毫无疑问我们的是啊
return dispatchTouchEvent(event);
} else {
return dispatchGenericMotionEvent(event);
}
}
这个时候我们要去看View.dispatchTouchEvent吗??NO!!!!!我们应该看DecorView.dispatchTouchEvent(DecorView重写了dispatchTouchEvent)
DecorView.dispatchTouchEvent声明如下
DecorView.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
/**获取Window.Callback,Window.Callback是个接口,这里的mWindow是PhoneWindow,调用*
*PhoneWindow.getCallback(),但是PhoneWindow并没有实现该方法,所以我们找到了
*Window.getCallBack()方法。Window.getCallBack()方法返回Callback类型的mCallback
*/
final Window.Callback cb = mWindow.getCallback();
/**
*如果cb不为空并且window没有被销毁 mFeatureId < 0 表示是application的DecorView,
*比如Activity、Dialog把事件传给cb,否则把事件传递给父类的dispatchTouchEvent
*/
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}
Window.java
public final Callback getCallback() {
return mCallback;
}
Callback.java
public interface Callback {
...//省略一部分函数
public boolean dispatchTouchEvent(MotionEvent event);
...
}
我们来看这个Window.Callback ,既然有getCallback(),那么应该有setCallback为mCallback赋值。我们
我们在Activity的attach方法中看到如下代码
Activity.java
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback) {
......
//创建PhoneWindow
mWindow = new PhoneWindow(this, window, activityConfigCallback);
......
//设置当前Activity为Window.Callback,那么毫无疑问,Activity类或者其父类实现了Window.Callback接口
mWindow.setCallback(this);
......
}
我们来看Activity类的声明
果然如此。那么我们就来看看Activity.dispatchTouchEvent
Activity.java
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//在这里我们又把事件给了PhoneWindow.superDispatchTouchEvent方法根据其返回值,
//若返回值为true,那么dispatchTouchEvent返回true,我们Activity的onTouchEvent方法无法得到执行
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//这里就是我们的Activity的onTouchEvent方法
return onTouchEvent(ev);
}
那我们要看PhoneWindow.superDispatchTouchEvent
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
//兜兜转转一大圈,还是把事件交给我们的DecorView,
//DecorView继承自FrameLayout,FrameLayout呢又继承自ViewGroup,
//所以作为一个ViewGroup,DecorView继续向其子View派发事件,其流程我在文章的开头就已经给了
return mDecor.superDispatchTouchEvent(event);
}
总结:兜兜转转一大圈我们神经都被绕弯了,我们在这里总结一下,当我们触摸(点击)屏幕时,Android输入系统IMS通过对事件的加工处理再合适的Window接收者并通过InputChannel向Window派发加工后的事件,并触发InputReceiver的onInputEvent的调用,由此产生后面一系列的调用,把事件派发给整个控件树的根DecorView。而DecorView又上演了一出偷梁换柱的把戏,先把事件交给Activity处理,在Activity中又把事件交还给了我们的DecorView。自此沿着控件树自上向下依次派发事件。
我们总算把ACTION_DOWN的事件分发分析完毕了,ACTION_DOWN事件可以说是所有触摸事件的起点。我们触摸了屏幕,并引发ACTION_DOWN的事件,然后可能经过一系列的ACTION_MOVE事件,最后是ACTION_UP事件,至ACTION_UP,这整个事件序列算是完成了。我们前面分析了ACTION_DOWN事件,那么ACTION_MOV和ACTION_UP呢,ACTION_MOV和ACTION_UP的事件分发与ACTION_DOWN并不完全相同。为什么这么说呢,是因为他们很相似,但是稍微有些不同。你在执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个事件(如ACTION_DOWN)返回true,才会收到ACTION_MOVE和ACTION_UP的事件。那么这句话是什么意思呢?我们来看一下不同情况下事件派发图。
我们在ViewGroup1中的dispatchTouchEvent中消费事件!
我们在ViewGroupX中的dispatchTouchEvent中消费事件!
我们在View中的dispatchTouchEvent中消费事件!
我们在View中的onTouchEvent中消费事件!
特殊情况1 :我们在ViewGroupX中的onTouchEvent中消费事件!
特殊情况2 :我们在ViewGroupX中的dispatchTouchEvent中返回false并在ViewGroup1中的onTouchEvent中消费事件!
还有种种情况我就不画图了。。为什么会产生上面的结果呢?我们还是来看一下ViewGroup的dispatchTouchEvent源码把。
ViewGroup dispatchTouchEvent源码分析
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
......
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
//表示窗口是否为模糊窗口(FILTER_TOUCHES_WHEN_OBSCURED),
//如果是窗口,则表示不希望处理改事件。(如dialog后的窗口)
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
/** 第①步 重新设置状态 开始*/
// 处理初始的按下动作
if (actionMasked == MotionEvent.ACTION_DOWN) {
//重新设置状态等,比较重要的是设置mFirstTouchTarget == null,
cancelAndClearTouchTargets(ev);
resetTouchState();
}
/** 第①步 重新设置状态 结束*/
/** 第②步 检查是否拦截 开始*/
// 检查是否拦截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {//如果是ACTION_DOWN事件或者mFirstTouchTarget != null
//这里我们去问ViewGroup是否允许拦截,如果允许拦截,我们再去问onInterceptTouchEvent
......
} else {
//如果不是MotionEvent.ACTION_DOWN事件并且mFirstTouchTarget 为空,直接拦截
intercepted = true;
}
/** 第②步 检查是否拦截 结束*/
......
/** 第③步 向子View派发 开始*/
if (!canceled && !intercepted) {//如果没有取消并且当前ViewGroup没有拦截事件
......
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
//判断事件类型,如果是ACTION_DOWN或者ACTION_POINTER_DOWN或者ACTION_HOVER_MOVE则进入
if (newTouchTarget == null && childrenCount != 0) {
......
......
//获取子View并循环向子View派发事件
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//如果当前ViewGroup的子View消费了事件,则进入if体
......
//赋值newTouchTarget和mFirstTouchTarget
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
......
}
}
/** 第③步 向子View派发 结束*/
/** 第④步 额外的处理 开始*/
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
/**这个判断十分重要:
*
*我们在上面的过程中就知道倘若我们没有拦截即intercepted = false;如果事件是ACTION_DOWN
*或者ACTION_POINTER_DOWN或者ACTION_HOVER_MOVE我们会进入循环子View并派发事件的过程,
*如果子View也不想处理该事件即dispatchTransformedTouchEvent()函数返回了false,那么此时ViewGroup的mFirstTouchTarget == null
*倘若我们重写了onInterceptTouchEvent并返回true,那么intercepted = true即进行拦截,
*那么就不会进入我们的第③步,直接来到第④步,这时当前ViewGroup的mFirstTouchTarget == null
mFirstTouchTarget == null的条件下会调用dispatchTransformedTouchEvent
*/
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
/** 第④步 额外的处理 结束*/
......
return handled;
}
我们从上面的代码可以更清晰的了解到ACTION_DOWN的派发过程,现在还存疑的就是这个mFirstTouchTarget了,我们在触发ACTION_DOWN的时候,ViewGroup会根据事件掩码actionMask判断ACTION_DOWN,并重置一些状态,重置状态的过程中就包括把mFirstTouchTarget设为null,我们第一次进入第三步时找到合适的子View并向其派发事件,如果子View消费了ACTION_DOWN事件,则调用addTouchTarget进行赋值,我们来看一下这个函数
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
//在这里我们可以看到mFirstTouchTarget 指向了子View
mFirstTouchTarget = target;
return target;
}
有上面的代码可知mFirstTouchTarget是ViewGroup的一个成员变量,每一个ViewGroup都持有这个mFirstTouchTarget。
这个mFirstTouchTarget是个单向链表,表示的是当前ViewGroup的子View有没有消费ACTION_DOWN事件,如果消费了ACTION_DOWN事件,就如上面代码中第③步的时候描述的一样给mFirstTouchTarget赋值,如果当前ViewGroup的子View没有消费ACTION_DOWN事件,即把事件分发给子View的这个dispatchTransformedTouchEvent()函数返回了false,不进入if体,mFirstTouchTarget还是为null。
我们接着来看第④步,结合上图中的特殊情况1,我们在ViewGroupX中的onTouchEvent中消费了事件。那么对于ViewGroupX来说,它的mFirstTouchTarget==null,因为它的子View并没有消费事件,对于ViewGroup1来说它的mFirstTouchTarget != null,因为它的子View ViewGroupX消费了事件,以此类推最后得到的mFirstTouchTarget 链表类似于下图
由于ACTION_MOVE|ACTION_UP事件不符合第③步时进入获取子View并循环派发的条件,当是ACTION_MOVE|ACTION_UP事件会直接来到第④步,判断当前ViewGroup的mFirstTouchTarget 是否为空,由上图可知不为空,那么进入第④步else体,在第④步else体内依据下图的链表逐一向子View派发事件。所以ACTION_MOVE|ACTION_UP事件只派发到ViewGroupX并交由ViewGroupX的onTouchEvent处理,不再向下派发。
那我们再来看一下mFirstTouchTarget == null的条件下调用的dispatchTransformedTouchEvent函数
参数分别是ev, canceled, null, TouchTarget.ALL_POINTER_IDS
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
//child为空调用父类即View的dispatchTouchEvent
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
if (newPointerIdBits == 0) {
return false;
}
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
//child为空调用父类即View的dispatchTouchEvent
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
if (child == null) {
//child为空调用父类即View的dispatchTouchEvent
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
上面的函数我们就不仔细分析了,不过注释里写的很明白,只要流程正常的话,我们都会调用父类的dispatchTouchEvent
我们来看一下View的dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
......
if (onFilterTouchEventForSecurity(event)) {
......
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {//这里判断有没有为View是否可用Enabled并且检查是否设置了TouchListener,如果设置了,则触发TouchListener的onTouch
result = true;
}
if (!result && onTouchEvent(event)) {//如果当前View没有设置listener信息,事件也没有被滚动条消费这里回调了我们的onTouchEvent。所以如果为当前View设置了TouchListenerb并在TouchListener的onTouch函数中返回了true,那么,该View的onTouchEvent将无法得到回调。
result = true;
}
}
......
return result;
}
本篇总结
本篇文章详细分析了View的事件体系(写这一篇文章真是不容易啊)。作为所有触摸事件的起点ACTION_DOWN|ACTION_POINTER_DOWN来说,Android对其的处理很精细,尤其是ViewGroup对其的处理。
-
首先重置状态,这是因为一个新的事件序列开始了,重置状态中比较重要的就是这个mFirstTouchTarget了,mFirstTouchTarget作为ViewGroup的成员变量记录当前ViewGroup下的子View是否消费了该ACTION_DOWN|ACTION_POINTER_DOWN事件。这个子View的意思也不仅仅是直接子View。假如有这样一个结构
<ViewGroup1>
<ViewGroup2> <ViewGroup3> <ViewGroup4> <View> </View> </ViewGroup4> </ViewGroup3> </ViewGroup2>
</ViewGroup1>
假设是View消费了ACTION_DOWN|ACTION_POINTER_DOWN事件,那么ViewGroup1的mFirstTouchTarget就是ViewGroup2->ViewGroup3->ViewGroup4->View
- 如果ViewGroup子View消费了事件,那么记录mFirstTouchTarget,ACTION_DOWN|ACTION_POINTER_DOWN事件结束,如果没有子View消费此事件,mFirstTouchTarget为null。后续的ACTION_MOVE|ACTION_UP事件会根据上一步中的mFirstTouchTarget进行分发。若为null,调用父类的即View的dispatchTouchEvent,该函数内部会先判断Listener信息,并调用listener的onTouch方法,根据onTouch的返回值决定是否继续调用当前ViewGroup的onTouchEvent方法;若不为null,则根据mFirstTouchTarget链表进行分发后续的ACTION_MOVE|ACTION_UP事件。
希望读者能多看几遍上面的分析。相信你一定会有收获的
下篇预告
在下一篇文章中我们将进行实战项目,也是对我们前几篇文章的实际应用。老话说的好,纸上得来终觉浅,绝知此事要躬行。下一篇甚至几篇我们就来自定义ViewGroup并重点探讨滑动冲突如何解决。滑动冲突解决的基础是今天这篇的View事件体系
参考博文
此致,敬礼
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。